import * as THREE from 'three/build/three.module'
import {CSS2DRenderer, CSS2DObject} from 'three/examples/jsm/renderers/CSS2DRenderer.js'
import {CSS3DRenderer, CSS3DObject} from 'three/examples/jsm/renderers/CSS3DRenderer.js'
import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls.js'
import {FirstPersonControls} from 'three/examples/jsm/controls/FirstPersonControls'
import {PointerLockControls} from 'three/examples/jsm/controls/PointerLockControls'
import {Sky} from 'three/examples/jsm/objects/Sky.js'
import {EffectComposer} from 'three/examples/jsm/postprocessing/EffectComposer'
import {OutlinePass} from 'three/examples/jsm/postprocessing/OutlinePass'
import {UnrealBloomPass} from 'three/examples/jsm/postprocessing/UnrealBloomPass'
import {RenderPass} from 'three/examples/jsm/postprocessing/RenderPass.js'
import {ShaderPass} from 'three/examples/jsm/postprocessing/ShaderPass.js'
import {FXAAShader} from 'three/examples/jsm/shaders/FXAAShader'
import {MTLLoader} from 'three/examples/jsm/loaders/MTLLoader'
import {OBJLoader} from 'three/examples/jsm/loaders/OBJLoader'
import {GLTFLoader} from 'three/examples/jsm/loaders/GLTFLoader'
import {FBXLoader} from 'three/examples/jsm/loaders/FBXLoader'
import {DRACOLoader} from 'three/examples/jsm/loaders/DRACOLoader'
import {TWEEN} from 'three/examples/jsm/libs/tween.module.min'

import html2canvas from 'html2canvas'

import {FirstPersonCameraControls} from './js/control/firstPersonCameraControls'
import {FirstPersonCameraControl} from './js/control/firstPersonCameraControl'
import {ThirdPersonCameraControl} from './js/control/thirdPersonCameraControl'

export class ThreeJsUtil {
    /**
     * 类初始化
     */
    constructor() {
        this.three = {}
        this.methods = {}
        this.events = {}
        this.params = {}

        this.container = ''
        this.scene = ''
        this.camera = ''
        this.renderer = ''

        this.onEvents = {}

        this.clock = new THREE.Clock()
    }

    /**
     * threeJs初始化
     * @param data {*}
     * @param callback function (scene, camera, renderer)
     */
    init(data, callback) {
        this.three = data.three
        this.methods = data.methods
        this.events = data.events
        this.params = data.params

        this.container = data.params.container
        if (data.params.containerId) this.container = document.getElementById(data.params.containerId)

        const width = this.container.offsetWidth, height = this.container.offsetHeight, asp = width / height

        this.scene = new THREE.Scene() // 初始化场景

        this.camera = new THREE.PerspectiveCamera(45, asp, 1, 25000) // 初始化相机

        this.addMethods()
        this.addEvent()

        callback(this.scene, this.camera, this.renderer)
    }

    /**
     * 添加方法（需添加的方法在init函数的data.methods中添加）
     * @protected
     */
    addMethods() {
        const methods = this.methods
        if (methods.renderer) this.addRenderer()
        if (methods.event) this.addEvent()
        if (methods.light) this.addLight()
        if (methods.controls) this.addControls()
        if (methods.sky) this.addSky()
        if (methods.animation) this.animate()
        if (methods.postprocessing) this.addPostprocessing()
        if (methods.statement) this.statement()
    }

    /**
     * 加载模型
     * @param params （url, type, animations, scene） {url: 'http://www.model.cn' or {mtl: 'http://www.model.mtl.cn', obj: 'http://www.model.obj.cn'}, type: 'gltf', animations: true, scene: null}
     * @param callback
     * @param onProgress function
     * @return {obj, animations, mixer, actions}
     */
    addModel(params, callback, onProgress) {
        const that = this
        params = params || {}
        const url = params.url
        if (!url) throw '模型url不能为空'
        const filename = url.split('/').pop()
        const type = params.type || filename.split('.').pop().toLowerCase()
        const name = params.name || filename.split('.')[0]
        const scene = params.scene ? params.scene : this.scene

        var obj, animations, mixer, actions

        switch (type) {
            case 'gltf':
            case 'GLTF':
            case 'glb':
            case 'GLB':
                new GLTFLoader()
                    .load(url, function (gltf) {
                        obj = gltf.scene
                        animations = gltf.animations
                        obj.name = name
                        scene.add(obj)
                        if (params.animations) {
                            const res = that.addAnimation(obj, animations)
                            mixer = res.mixer
                            actions = res.actions
                        }
                        callback(obj, animations, mixer, actions)
                    }, (progress) => {
                        if (onProgress) onProgress(progress)
                    })
                break
            case 'fbx':
            case 'FBX':
                new FBXLoader()
                    .load(url, function (fbx) {
                        obj = fbx
                        animations = fbx.animations
                        obj.name = name
                        scene.add(obj)
                        if (params.animations) {
                            const res = that.addAnimation(obj, animations)
                            mixer = res.mixer
                            actions = res.actions
                        }
                        callback(obj, animations, mixer, actions)
                    }, (progress) => {
                        if (onProgress) onProgress(progress)
                    })
                break
            case 'draco-gltf':
            case 'DRACO-GLTF':
                var dracoLoader = new DRACOLoader()
                dracoLoader.setDecoderPath('static/draco/gltf/') // 设置解压库文件路径
                dracoLoader.preload()
                new GLTFLoader()
                    .setDRACOLoader(dracoLoader)
                    .load(url, function (gltf) {
                        obj = gltf.scene
                        animations = gltf.animations
                        obj.name = name
                        scene.add(obj)
                        if (params.animations) {
                            const res = that.addAnimation(obj, animations)
                            mixer = res.mixer
                            actions = res.actions
                        }
                        callback(obj, animations, mixer, actions)
                    }, (progress) => {
                        if (onProgress) onProgress(progress)
                    })
                break
            case 'obj':
            case 'OBJ':
                new OBJLoader()
                    .load(url, function (object) {
                        obj = object
                        obj.name = name
                        callback(obj, animations, mixer, actions)
                    }, (progress) => {
                        if (onProgress) onProgress(progress)
                    })
                break
            case 'mtl-obj':
            case 'MTL-OBJ':
                new MTLLoader()
                    .load(url.mtl, function (materials) {
                        materials.preload()
                        new OBJLoader()
                            .setMaterials(materials)
                            .load(url.obj, function (object) {
                                obj = object
                                obj.name = name
                                callback(obj, animations, mixer, actions)
                            }, (progress) => {
                                if (onProgress) onProgress(progress)
                            })
                    })
                break
            default:
                throw `不支持的模型格式：${type}`
        }
        // if (type === 'gltf' || type === 'glb') {
        //     // console.log('开始加载' + type + '格式模型')
        // } else if (type === 'fbx') {
        //     // console.log('开始加载' + type + '格式模型')
        // } else if (type === 'draco-gltf') {
        //     // console.log('开始加载' + type + '格式模型')
        // } else if (type === 'obj') {
        //     // console.log(`开始加载${type}格式模型`)
        // } else if (type === 'mtl-obj') {
        //     // console.log('开始加载' + type + '格式模型')
        // }
    }

    /**
     * 添加模型模型监听事件
     * @param obj object 需要监听的模型集合
     * @param type str 事件类型
     * @param exist function 事件触发时，监听到模型时的触发函数
     * @param key str 事件标识符
     * @param params {*}
     * @param lose function 事件触发时，未监听到模型时的触发函数
     */
    addOn(obj, type, exist, key, params, lose) {
        if (this.onEvents[type + '_' + key]) {
            throw `事件监听${type}_${key}已存在`
            return
        }
        const that = this
        obj = obj.children.length ? obj : {children: [obj]}
        params = params ? params : {}
        const dom = params.dom ? params.dom : this.container
        const recursive = params.recursive || false
        this.onEvents[type + '_' + key] = (event) => {
            const objList = that.getIntersectObject(event, obj, recursive).objectList
            if (objList.length > 0) {
                exist(objList)
            } else {
                lose ? lose() : null
            }
            // console.log(type + '监听事件未拾取到对象。此消息来自：' + key)
        }
        dom.addEventListener(type, this.onEvents[type + '_' + key])
    }

    /**
     * 移除监听事件
     * @param type str 事件类型
     * @param key str 事件标识符
     * @param params {*}
     */
    rmOn(type, key, params) {
        if (this.onEvents[type + '_' + key]) {
            params = params ? params : {}
            const dom = params.dom ? params.dom : this.container
            dom.removeEventListener(type, this.onEvents[type + '_' + key])
            this.onEvents[type + '_' + key] = null
        } else {
            throw `事件监听${type}_${key}不存在`
        }
    }

    /**
     * 添加动画
     * @param model
     * @param animations
     * @return {mixer, actions}
     */
    addAnimation(model, animations) {
        const mixer = new THREE.AnimationMixer(model) // 创建混合器

        var actions = {}

        for (var i = 0; i < animations.length; i++) {
            const clip = animations[i]
            actions[clip.name] = mixer.clipAction(clip) // 返回动画操作对象
        }
        return {
            mixer,
            actions
        }
    }

    /**
     * 添加监听事件
     * @protected
     */
    addEvent() {
        const events = this.events
        if (events.windowResize) this.onWindowResize()
    }

    /**
     * 添加后置处理
     * @param callback
     * @param params {*} {scene}
     * @return {EffectComposer}
     */
    addPostprocessing(callback, params) {
        const that = this
        params = params || {}
        const scene = params.scene || this.scene
        const renderer = params.renderer || this.renderer
        const camera = params.camera || this.camera

        this.composer = new EffectComposer(renderer) // 效果组合对象

        var renderScene = new RenderPass(scene, camera) // 新建场景通道
        this.composer.addPass(renderScene)

        // 抗锯齿
        this.effectFXAA = new ShaderPass(FXAAShader) // 着色通道
        this.effectFXAA.uniforms['resolution'].value.set(1 / this.container.offsetWidth, 1 / this.container.offsetHeight)
        this.composer.addPass(this.effectFXAA)

        this.params.postprocessing.forEach((pass) => {
            if (pass === 'OutlinePass') {
                that.outlinePass = new OutlinePass(new THREE.Vector2(that.container.offsetWidth, that.container.offsetHeight), scene, camera) // 边缘发光通道
                that.composer.addPass(that.outlinePass)

                that.outlinePass.edgeStrength = 5 // 0.01-10
                that.outlinePass.edgeGlow = 0.5 // 0-1
                that.outlinePass.edgeThickness = 1 // 1-4
                that.outlinePass.pulsePeriod = 2 // 0-5
                that.outlinePass.usePatternTexture = false
                that.outlinePass.visibleEdgeColor.set('#18f2f2')
                that.outlinePass.hiddenEdgeColor.set('#ff0404')
            } else if (pass === 'UnrealBloomPass') {
                renderer.toneMapping = THREE.ReinhardToneMapping

                that.unrealBloomPass = new UnrealBloomPass(new THREE.Vector2(that.container.offsetWidth, that.container.offsetHeight), 1.5, 0.4, 0.85) // 边缘发光通道
                that.composer.addPass(that.unrealBloomPass)

                that.unrealBloomPass.renderToScreen = true
                renderer.toneMappingExposure = 1
                that.unrealBloomPass.threshold = 0
                that.unrealBloomPass.strength = 1.5
                that.unrealBloomPass.radius = 0
            }
        })

        if (callback) callback()

        return this.composer
    }

    /**
     * 添加边缘发光通道
     * @param callback
     * @param params
     * @returns {*}
     */
    addOutlinePass(callback, params) {
        params = params || {}
        const scene = params.scene || this.scene
        const renderer = params.renderer || this.renderer
        const camera = params.camera || this.camera
        const render = params.render || false

        const composer = params.composer || new EffectComposer(renderer) // 效果组合对象

        if (render) {
            var renderScene = new RenderPass(scene, camera) // 新建场景通道
            composer.addPass(renderScene)
        }

        const outlinePass = new OutlinePass(new THREE.Vector2(this.container.offsetWidth, this.container.offsetHeight), scene, camera) // 边缘发光通道
        composer.addPass(outlinePass)

        // 抗锯齿
        // this.effectFXAA = new ShaderPass(FXAAShader) // 着色通道
        // this.effectFXAA.uniforms['resolution'].value.set(1 / this.container.offsetWidth, 1 / this.container.offsetHeight)
        // composer.addPass(this.effectFXAA)

        outlinePass.edgeStrength = 5 // 0.01-10
        outlinePass.edgeGlow = 0.5 // 0-1
        outlinePass.edgeThickness = 1 // 1-4
        outlinePass.pulsePeriod = 2 // 0-5
        outlinePass.usePatternTexture = false
        outlinePass.visibleEdgeColor.set('#18f2f2')
        outlinePass.hiddenEdgeColor.set('#ff0404')

        if (callback) callback()

        return {composer, outlinePass}
    }

    /**
     * 添加局部辉光通道
     * @param callback
     * @param params
     * @returns {*}
     */
    addUnrealBloomPass(callback, params) {
        params = params || {}
        const scene = params.scene || this.scene
        const renderer = params.renderer || this.renderer
        const camera = params.camera || this.camera
        const render = params.render || false

        renderer.toneMapping = THREE.ReinhardToneMapping
        const composer = params.composer || new EffectComposer(renderer) // 效果组合对象

        if (render) {
            var renderScene = new RenderPass(scene, camera) // 新建场景通道
            composer.addPass(renderScene)
        }

        const unrealBloomPass = new UnrealBloomPass(new THREE.Vector2(this.container.offsetWidth, this.container.offsetHeight), 1.5, 0.4, 0.85) // 边缘发光通道
        composer.addPass(unrealBloomPass)

        unrealBloomPass.renderToScreen = false
        renderer.toneMappingExposure = 1
        unrealBloomPass.threshold = 0
        unrealBloomPass.strength = 1.5
        unrealBloomPass.radius = 0

        if (callback) callback()

        return {composer, unrealBloomPass}
    }

    /**
     * 添加天空
     */
    addSky() {
        const that = this
        // Add Sky
        this.sky = new Sky()
        this.sky.scale.setScalar(450000)
        this.scene.add(this.sky)

        // Add Sun Helper
        this.sunSphere = new THREE.Mesh(
            new THREE.SphereBufferGeometry(20000, 16, 8),
            new THREE.MeshBasicMaterial({color: 0xffffff})
        )
        this.sunSphere.position.y = -700000
        this.sunSphere.visible = false
        this.scene.add(this.sunSphere)

        var distance = 400000

        var uniforms = that.sky.material.uniforms
        uniforms['turbidity'].value = 10
        uniforms['rayleigh'].value = 2
        uniforms['mieCoefficient'].value = 0.005
        uniforms['mieDirectionalG'].value = 0.8
        uniforms['luminance'] = {value: 1}

        var theta = Math.PI * (0.49 - 0.5)
        var phi = 2 * Math.PI * (0.25 - 0.5)

        that.sunSphere.position.x = distance * Math.cos(phi)
        that.sunSphere.position.y = distance * Math.sin(phi) * Math.sin(theta)
        that.sunSphere.position.z = distance * Math.sin(phi) * Math.cos(theta)

        that.sunSphere.visible = false

        uniforms['sunPosition'].value.copy(that.sunSphere.position)
    }

    /**
     * 添加渲染器
     * @param callback
     */
    addRenderer(callback) {
        const that = this
        this.params.renderer.forEach((renderer) => {
            if (renderer === 'WebGLRenderer') {
                that.renderer = new THREE.WebGLRenderer(that.params.rendererParams || null) // 初始化渲染器
                that.renderer.setSize(that.container.offsetWidth, that.container.offsetHeight)
                that.container.appendChild(that.renderer.domElement)
            } else if (renderer === 'CSS2DRenderer') {
                that.CSS2DRenderer = new CSS2DRenderer()
                that.CSS2DRenderer.domElement.style.position = 'absolute'
                that.CSS2DRenderer.domElement.style.top = 0
                that.CSS2DRenderer.setSize(that.container.offsetWidth, that.container.offsetHeight)
                that.container.appendChild(that.CSS2DRenderer.domElement)
            } else if (renderer === 'CSS3DRenderer') {
                that.CSS3DRenderer = new CSS3DRenderer()
                that.CSS3DRenderer.domElement.style.position = 'absolute'
                that.CSS3DRenderer.domElement.style.top = 0
                that.CSS3DRenderer.setSize(that.container.offsetWidth, that.container.offsetHeight)
                that.container.appendChild(that.CSS3DRenderer.domElement)
            }
        })
    }

    /**
     * 添加控制器（内部调用允许重写，外部调用允许回调）
     * @return {*}
     */
    addControls(callback) {
        const that = this
        this.params.controls.forEach((controls) => {
            if (controls === 'OrbitControls') {
                const orbitControls = new OrbitControls(this.camera, this.container)
                that.orbitControls = orbitControls
                // 如果使用animate方法时，将此函数删除
                // this.controls.addEventListener('change', render)
                // 使动画循环使用时阻尼或自转 意思是否有惯性
                orbitControls.enableDamping = true
                // 动态阻尼系数 就是鼠标拖拽旋转灵敏度
                // this.orbitControls.dampingFactor = 0.25
                // 是否可以缩放
                orbitControls.enableZoom = true
                // // 是否自动旋转
                orbitControls.autoRotate = true
                orbitControls.autoRotateSpeed = 1
                // 设置相机距离原点的最近距离
                orbitControls.minDistance = 1
                // 设置相机距离原点的最远距离
                // orbitControls.maxDistance = 2000
                // 是否开启右键拖拽
                // orbitControls.enablePan = true
            } else if (controls === 'FirstPersonControls') {
                const firstPersonControls = new FirstPersonControls(this.camera)
                that.firstPersonControls = firstPersonControls
                firstPersonControls.enabled = true
                firstPersonControls.lookSpeed = 0.02 // 鼠标移动查看的速度
                firstPersonControls.movementSpeed = 10 //相机移动速度
                firstPersonControls.noFly = false
                firstPersonControls.constrainVertical = true // 约束垂直
                firstPersonControls.verticalMin = 1.0
                firstPersonControls.verticalMax = 2.0
                firstPersonControls.lon = 0 // 进入初始视角x轴的角度
                firstPersonControls.lat = 0 // 初始视角进入后y轴的角度
            } else if (controls === 'PointerLockControls') {
                const pointerLockControls = new PointerLockControls(this.camera, this.container)
                that.pointerLockControls = pointerLockControls
                this.scene.add(pointerLockControls.getObject())
            } else if (controls === 'firstPerson') {
                const firstPerson = new FirstPersonCameraControls(this.camera, this.container)
                that.firstPerson = firstPerson
            } else if (controls === 'thirdPerson') {
                const thirdPerson = new ThirdPersonCameraControl(this.camera, that.orbitControls)
                that.thirdPerson = thirdPerson
            } else if (controls === 'firstPerson2') {
                const firstPerson2 = new FirstPersonCameraControl(this.camera, this.container)
                that.firstPerson2 = firstPerson2
            }
        })

        if (callback) callback()
        return this.controls
    }

    /**
     * 添加灯光（内部调用允许重写，外部调用允许回调）
     * @param params (scene)
     * @param callback
     * @return
     */
    addLight(callback, params) {
        params = params || {}
        const scene = params.scene ? params.scene : this.scene

        const ambientLight = new THREE.AmbientLight('#fff', 1)
        scene.add(ambientLight)
        if (callback) callback()
        return ambientLight
    }

    /**
     * 窗口尺寸（内部调用允许重写，外部调用允许回调）
     * @param callback
     */
    onWindowResize(callback) {
        const that = this
        window.addEventListener('resize', (e) => {
            const width = that.container.offsetWidth, height = that.container.offsetHeight
            that.camera.aspect = width / height
            that.camera.updateProjectionMatrix()
            that.renderer.setSize(width, height)
            if (that.methods.CSS2DRenderer) that.CSS2DRenderer.setSize(width, height)
            if (that.methods.CSS3DRenderer) that.CSS3DRenderer.setSize(width, height)
            if (that.composer) {
                that.composer.setSize(width, height)
                that.effectFXAA.uniforms['resolution'].value.set(1 / width, 1 / height)
            }
            if (callback) callback()
        }, false)
    }

    /**
     * 动画（内部调用允许重写，外部调用允许回调）
     * @param callback
     */
    animate(callback) {
        const that = this
        requestAnimationFrame(() => {
            that.animate(callback)
        })
        that.render()
        if (that.methods.TWEEN) TWEEN.update()

        if (that.methods.controls) {
            if (that.orbitControls) {
                if (that.orbitControls.enabled === true) {
                    that.orbitControls.update()
                }
            }
            if (that.firstPersonControls) {
                if (that.firstPersonControls.enabled === true) {
                    that.firstPersonControls.update(that.clock.getDelta())
                }
            }
            if (that.pointerLockControls) {
                if (that.pointerLockControls.enabled === true) {
                }
            }
            if (that.firstPerson) {
                if (that.firstPerson.enabled === true) {
                    that.firstPerson.update()
                }
            }
            if (that.firstPerson2) {
                if (that.firstPerson2.enabled === true) {
                    that.firstPerson2.update()
                }
            }
            if (that.thirdPerson) {
                if (that.thirdPerson.enabled === true) {
                    that.thirdPerson.update()
                }
            }
        }

        if (callback) callback()
    }

    /**
     * 渲染（内部调用允许重写，外部调用允许回调）
     * @param callback
     */
    render(callback) {
        if (this.methods.renderer) {
            if (this.CSS2DRenderer) this.CSS2DRenderer.render(this.scene, this.camera)
            if (this.CSS3DRenderer) this.CSS3DRenderer.render(this.scene, this.camera)
            if (this.renderer) this.renderer.render(this.scene, this.camera)
        }

        if (this.composer) this.composer.render()
        if (callback) callback()
    }

    /**
     * 销毁
     * @param callback
     */
    destroy (callback) {
        // 停止渲染
        cancelAnimationFrame(this.animationFrameCount)
        // 销毁场景子元素
        this.scene.children.forEach((item) => {
            this.scene.remove(item)
            item = null
        })
        // 销毁场景
        this.scene = null

        // 销毁相机
        this.camera = null

        // 销毁渲染器
        if (this.methods.renderer) {
            if (this.renderer) {
                this.renderer.dispose()
                this.renderer = null
            }
            if (this.CSS2DRenderer) {
                this.CSS2DRenderer.dispose()
                this.CSS2DRenderer = null
            }
            if (this.CSS3DRenderer) {
                this.CSS3DRenderer.dispose()
                this.CSS3DRenderer = null
            }
        }

        // 销毁控制器
        if (this.methods.controls) {
            if (this.orbitControls) {
                this.orbitControls.dispose()
                this.orbitControls = null
            }
            if (this.firstPersonControls) {
                this.firstPersonControls.dispose()
                this.firstPersonControls = null
            }
            if (this.pointerLockControls) {
                this.pointerLockControls.dispose()
                this.pointerLockControls = null
            }
            if (this.firstPerson) {
                this.firstPerson.removeEvents()
                this.firstPerson = null
            }
            if (this.thirdPerson) {
                this.thirdPerson.removeEvents()
                this.thirdPerson = null
            }
        }

        // 销毁后期处理
        if (this.methods.postprocessing) {
            if (this.outlinePass) {
                this.outlinePass.dispose()
                this.outlinePass = null
            }
            if (this.unrealBloomPass) {
                this.unrealBloomPass.dispose()
                this.unrealBloomPass = null
            }
        }

        if (callback) callback()
    }

    /**
     * 获取模型层级
     * @param model 模型
     * @return {*}
     */
    getModelTier(model) {
        const that = this
        if (model.type === 'Group') {
            var children = []
            model.children.forEach((item) => {
                children.push(that.getModelTier(item))
            })
            return {name: model.name, type: model.type, ID: model.ID, children}
        } else if (model.type === 'Mesh') {
            return {name: model.name, type: model.type, ID: model.ID}
        }
    }

    /**
     * 向树中每一级添加父级元素
     * @param tree Json
     * @param type String list or dict
     * @return {*}
     */
    addParentToTree(tree, type) {
        const that = this
        type = type || 'dist'
        if (type === 'dict' && tree.children) {
            tree.children.forEach((newTree) => {
                newTree.parent = tree
                that.addParentToTree(newTree, 'dict')
            })
        } else if (type === 'list') {
            tree.forEach((newTree) => {
                that.addParentToTree(newTree, 'dict')
            })
        }

        return tree
    }

    /**
     * 创建精灵文字
     * @param text str
     * @param options (fontSize,backgroundColor,color) {fontSize:20, backgroundColor: '#fff', color: '#fff'}
     * @returns {*}
     */
    createSpriteText(text, options) {
        if (!options) options = {}
        options.fontSize = options.fontSize || 12 // 字体大小，默认12
        const name = options.name || ''
        const scene = options.scene || this.scene
        const scale = options.scale || [100, 100, 1]
        const position = options.position || [0, 0, 0]

        const average = this.container.offsetWidth > this.container.offsetHeight ? this.container.offsetHeight / 180 : this.container.offsetWidth / 360
        const canvas = document.createElement('canvas')
        canvas.width = text.length * (options.fontSize || 18) * average // 画布宽，根据内容长度和字体大小自动调整
        canvas.height = options.fontSize * average // 画布高，根据字体大小自动调整
        const ctx = canvas.getContext('2d')
        ctx.fillStyle = options.backgroundColor || 'transparent' // 画布背景色，默认为透明色
        ctx.fillRect(0, 0, canvas.width, canvas.height) // 绘制矩阵，params:矩形的左上角的坐标，矩形的大小
        ctx.font = canvas.height + 'px \'微软雅黑\'' // 字体
        ctx.textAlign = 'center' // 文字排列
        ctx.textBaseline = 'middle' // 文本基线
        ctx.fillStyle = options.color // 字体颜色
        ctx.fillText(text, canvas.width / 2, canvas.height / 2)
        const texture = new THREE.Texture(canvas)
        texture.needsUpdate = true
        const sprite = new THREE.Sprite(new THREE.SpriteMaterial({map: texture}))
        sprite.scale.set(options.fontSize / average * text.length, options.fontSize / average, 1) // 缩放比例
        sprite.name = name
        scene.add(sprite)
        sprite.scale.set(scale[0], scale[1], scale[2]) // 缩放比例
        sprite.position.set(position[0], position[1], position[2]) // 模型位置
        return sprite
    }

    /**
     * 创建精灵标签
     * @param title str 标题
     * @param content str 内容
     * @param options (fontSize,backgroundColor,color,scene,scale,position) {fontSize:20, width: 200, backgroundColor: '#fff', color: '#fff'}
     * @returns {*}
     */
    createSpriteLabel(title, content, options) {
        if (!options) options = {}
        options.fontSize = options.fontSize || 15 // 字体大小，默认15
        options.width = options.width || 200 // 画布宽度，默认200
        const name = options.name || ''
        const scene = options.scene || this.scene
        const scale = options.scale || [100, 100, 1]
        const position = options.position || [0, 0, 0]

        const average = 1.2
        var canvas = document.createElement('canvas')
        var context = canvas.getContext('2d')
        canvas.width = options.width
        var lineHeight = options.fontSize * average // 行高，根据字体大小自动调整
        context.font = lineHeight + 'px \'微软雅黑\''
        var result = this.breakLinesForCanvas(content, context,
            options.width)

        canvas.height = options.fontSize * 2 + (result.length + 1) * lineHeight
        context.fillStyle = options.backgroundColor || 'rgba(0, 127, 127, 0.7)' // 画布背景色，默认为透明色
        context.fillRect(0, 0, canvas.width, canvas.height) // 绘制矩阵，params:矩形的左上角的坐标，矩形的大小
        context.fillStyle = options.color || '#06f5ed' // 字体颜色
        context.font = options.fontSize + 3 + 'px \'微软雅黑\''
        context.textBaseline = 'middle' // 文本基线
        context.fillText(title, 10, lineHeight)
        context.font = options.fontSize + 'px \'微软雅黑\''
        result.forEach(function (line, index) {
            context.fillText(line, 20, lineHeight * (index + 1) + options.fontSize * 2)
        })

        const texture = new THREE.Texture(canvas)
        texture.needsUpdate = true
        const sprite = new THREE.Sprite(new THREE.SpriteMaterial({map: texture}))
        sprite.name = name
        scene.add(sprite)
        sprite.scale.set(scale[0], scale[1], scale[2]) // 缩放比例
        sprite.position.set(position[0], position[1], position[2]) // 模型位置
        return sprite
    }

    /**
     * 创建div精灵标签
     * @param title str 标题
     * @param content str 内容
     * @param callback function 回调函数
     * @param options {*} (scene, dom)
     */
    createDivSpriteLabel(title, content, callback, options) {
        if (!options) options = {}
        const name = options.name || ''
        const scene = options.scene ? options.scene : this.scene
        const scale = options.scale || [100, 100, 1]
        const position = options.position || [0, 0, 0]

        const dom = options.dom ? options.dom : document.body
        const labelBox = document.createElement('div')
        labelBox.className = 'spriteLabel-box'
        dom.appendChild(labelBox)

        const titleBox = document.createElement('div')
        titleBox.className = 'spriteLabel-title-box'
        labelBox.appendChild(titleBox)

        const titleText = document.createElement('span')
        titleText.className = 'spriteLabel-title-text'
        titleText.textContent = title
        titleBox.appendChild(titleText)

        const contentBox = document.createElement('div')
        contentBox.className = 'spriteLabel-content-box'
        labelBox.appendChild(contentBox)

        const contentText = document.createElement('span')
        contentText.className = 'spriteLabel-content-text'
        contentText.textContent = content
        contentBox.appendChild(contentText)

        html2canvas(labelBox, {}).then((canvas) => {
            const texture = new THREE.Texture(canvas)
            texture.needsUpdate = true
            const sprite = new THREE.Sprite(new THREE.SpriteMaterial({map: texture}))
            sprite.name = name
            scene.add(sprite)
            sprite.scale.set(scale[0], scale[1], scale[2]) // 缩放比例
            sprite.position.set(position[0], position[1], position[2]) // 模型位置
            labelBox.parentNode.removeChild(labelBox)
            if (callback) callback(sprite)
        })
    }

    /**
     * 创建2D展示标签
     * @param title
     * @param content
     * @returns {CSS2DObject}
     */
    createShow2DLabel(title, content) {
        const labelBox = document.createElement('div')
        labelBox.className = 'label2d-box'

        const titleBox = document.createElement('div')
        titleBox.className = 'label2d-title-box'
        labelBox.appendChild(titleBox)

        const titleText = document.createElement('span')
        titleText.className = 'label2d-title-text'
        titleText.textContent = title
        titleBox.appendChild(titleText)

        const contentBox = document.createElement('div')
        contentBox.className = 'label2d-content-box'
        labelBox.appendChild(contentBox)

        const contentText = document.createElement('span')
        contentText.className = 'label2d-content-text'
        contentText.textContent = content
        contentBox.appendChild(contentText)

        const label2d = new CSS2DObject(labelBox)
        this.scene.add(label2d)
        label2d.scale.set(0.1, 0.1, 0.1) // 我们可以通过控制box的大小来控制label3d的大小
        return label2d
    }

    /**
     * 创建3D展示标签
     * @param title
     * @param content
     * @return {any}
     */
    createShow3DLabel(title, content) {
        const labelBox = document.createElement('div')
        labelBox.className = 'label3d-box'
        labelBox.style.backgroundColor = 'rgba(0,127,127,' + (Math.random() * 0.5 + 0.25) + ')'

        const titleBox = document.createElement('div')
        titleBox.className = 'label3d-title-box'
        labelBox.appendChild(titleBox)

        const titleText = document.createElement('span')
        titleText.className = 'label3d-title-text'
        titleText.textContent = title
        titleBox.appendChild(titleText)

        const contentBox = document.createElement('div')
        contentBox.className = 'label3d-content-box'
        labelBox.appendChild(contentBox)

        const contentText = document.createElement('span')
        contentText.className = 'label3d-content-text'
        contentText.textContent = content
        contentBox.appendChild(contentText)

        const label3d = new CSS3DObject(labelBox)
        this.scene.add(label3d)
        label3d.scale.set(0.1, 0.1, 0.1) // 我们可以通过控制box的大小来控制label3d的大小
        return label3d
    }

    /**
     * 三维坐标转屏幕坐标
     * @param vectorOrObject
     * @returns {{x: number, y: number}}
     */
    vectorToScreen(vectorOrObject) {
        let o
        if (vectorOrObject instanceof THREE.Vector3) {
            o = vectorOrObject
        } else if (vectorOrObject instanceof THREE.Object3D) {
            o = new THREE.Vector3(vectorOrObject.position.x, vectorOrObject.position.y, vectorOrObject.position.z)
        } else {
            throw  'the arguments is a object of Vector3 or Object3D'
        }
        const standardVector = o.project(this.camera) // 世界坐标转标准设备坐标
            , a = this.renderer.getSize(new THREE.Vector2()).width / 2
            , b = this.renderer.getSize(new THREE.Vector2()).height / 2
            , x = Math.round(standardVector.x * a + a) // 标准设备坐标转屏幕坐标
            , y = Math.round(-standardVector.y * b + b) // 标准设备坐标转屏幕坐标
        return {
            x,
            y
        }
    }

    /**
     * 地理坐标转2d平面
     * @param radius
     * @param lng
     * @param lat
     * @return {{x: number, y: number}}
     */
    geographicToPlaneCoords(radius, lng, lat) {
        return {x: (lat / 180) * radius, y: (lng / 180) * radius}
    }

    /**
     * 地理坐标转三维坐标
     * @param radius
     * @param lng
     * @param lat
     */
    geographicToVector(radius, lng, lat) {
        new THREE.Vector3().setFromSpherical(new THREE.Spherical(radius, (90 - lat) * (Math.PI / 180), (90 + lng) * (Math.PI / 180)))
    }

    /**
     * 地理坐标转三维坐标添加了高度
     * @param radius
     * @param lng
     * @param lat
     * @param height
     * @return {{x: number, y: number, z: number}}
     */
    geographicToVectorPosition(radius, lng, lat, height) {
        const phi = (180 + lng) * (Math.PI / 180)
        const theta = (90 - lat) * (Math.PI / 180)
        return {
            x: -radius * Math.sin(theta) * Math.cos(phi),
            y: radius * Math.cos(theta) + (height || 0),
            z: radius * Math.sin(theta) * Math.sin(phi)
        }
    }

    /**
     * 获取与射线相交的对象数组
     * @param event
     * @param parent
     * @param recursive
     * @returns {*}
     */
    getIntersectObject(event, parent, recursive) {
        event.preventDefault()
        const mouse = new THREE.Vector2()
        const raycaster = new THREE.Raycaster()
        let objectList = []
        try {
            mouse.x = (((event.clientX || (event.touches[0] ? event.touches[0].pageX : event.changedTouches[0].pageX)) - this.container.getBoundingClientRect().left) / this.container.offsetWidth) * 2 - 1
            mouse.y = -(((event.clientY || (event.touches[0] ? event.touches[0].pageY : event.changedTouches[0].pageY)) - this.container.getBoundingClientRect().top) / this.container.offsetHeight) * 2 + 1
            raycaster.setFromCamera(mouse, this.camera)
            // intersectObjects(object,recursive)object — 用来检测和射线相交的物体。recursive — 如果为true，它还检查所有后代。否则只检查该对象本身。缺省值为false。
            objectList = raycaster.intersectObjects((parent || this.scene).children, recursive)
        } catch (e) {
            // 鼠标越界
        }
        return {
            raycaster: raycaster,
            objectList: objectList
        }
    }

    /**
     * 从当前位置飞到指定位置
     * @param option
     * @returns {string|*}
     */
    flyTo(option) {
        const controls = this.orbitControls
        option.position = option.position || [] // 相机新的位置
        option.controls = option.controls || [] // 控制器新的中心点位置(围绕词典旋转等)
        option.duration = option.duration || 1000 // 飞行时间
        option.easing = option.easing || TWEEN.Easing.Linear.None
        TWEEN.removeAll()
        const curPosition = this.camera.position
        const controlsTar = controls.target
        const tween = new TWEEN.Tween({
            x1: curPosition.x, // 相机当前位置x
            y1: curPosition.y, // 相机当前位置y
            z1: curPosition.z, // 相机当前位置z
            x2: controlsTar.x, // 控制当前的中心点x
            y2: controlsTar.y, // 控制当前的中心点y
            z2: controlsTar.z // 控制当前的中心点z
        }).to({
            x1: option.position[0], // 新的相机位置x
            y1: option.position[1], // 新的相机位置y
            z1: option.position[2], // 新的相机位置z
            x2: option.controls[0], // 新的控制中心点位置x
            y2: option.controls[1], // 新的控制中心点位置x
            z2: option.controls[2] // 新的控制中心点位置x
        }, option.duration).easing(TWEEN.Easing.Linear.None) // TWEEN.Easing.Cubic.InOut //匀速
        tween.onUpdate(() => {
            controls.enabled = false
            this.camera.position.set(tween._object.x1, tween._object.y1, tween._object.z1)
            controls.target.set(tween._object.x2, tween._object.y2, tween._object.z2)
            controls.update()
            if (option.update instanceof Function) {
                option.update(tween)
            }
        })
        tween.onStart(() => {
            controls.enabled = false
            if (option.start instanceof Function) {
                option.start()
            }
        })
        tween.onComplete(() => {
            controls.enabled = true
            if (option.done instanceof Function) {
                option.done()
            }
        })
        tween.onStop(() => option.stop instanceof Function ? option.stop() : '')
        tween.start()
        TWEEN.add(tween)
        return tween
    }

    /**
     * 飞行到模型
     * @param obj object 模型对象
     * @param option ｛*｝
     */
    flyToObj(obj, option) {
        option = option || {}
        var flyToOption = option.flyToOption || {
            position: [100, 100, 100], // 相机新的位置
            controls: [0, 0, 0], // 控制器新的中心点位置
            duration: 3000, // 飞行时间
            start: () => {
            },
            update: () => {
            },
            done: () => {
            },
            stop: () => {
            }
        }
        const vector = option.vector || new THREE.Vector3(1, 0, 0)
        const radiusRatio = option.radiusRatio || 3
        const objCenter = this.getObjWorldPosition(obj)
        flyToOption['controls'] = [objCenter.x, objCenter.y, objCenter.z]
        var box = new THREE.Box3()
        const objRadius = box.expandByObject(obj).max.distanceTo(box.expandByObject(obj).min) / 2
        flyToOption['position'] = [vector.x * objRadius * radiusRatio + objCenter.x, vector.y * objRadius * radiusRatio + objCenter.y, vector.z * objRadius * radiusRatio + objCenter.z]

        this.flyTo(flyToOption)
    }

    /**
     * 获取模型对象世界坐标
     * @param obj {*} 模型对象
     * @param scene {*} 模型对象所在场景
     * @return {THREE.Vector3}
     */
    getObjWorldPosition(obj, scene) {
        scene = scene || this.scene

        scene.updateMatrixWorld(true)
        var worldPosition = new THREE.Vector3()
        obj.getWorldPosition(worldPosition)
        return worldPosition
    }

    /**
     * 获取两模型对象间距离
     * @param obj1 {*} 模型对象
     * @param obj2 {*} 模型对象
     * @param params {*}
     * @return {*}
     */
    getObjDistance(obj1, obj2, params) {
        params = params || {}
        const scene = params.scene || this.scene

        return this.getObjWorldPosition(obj1, scene).distanceTo(this.getObjWorldPosition(obj2, scene))
    }

    /**
     * 添加漫游
     * @param obj
     */
    addRoam(obj) {
        const that = this
        // 漫游终点
        const destination = new THREE.Mesh(new THREE.CircleGeometry(2.5, 50), new THREE.MeshBasicMaterial({
            color: 'green',
            transparent: true,
            opacity: 0.2,
            side: THREE.DoubleSide
        }))
        destination.visible = false
        destination.rotateX(-0.5 * Math.PI)
        destination.name = 'destination'
        this.scene.add(destination)
        // 漫游终点图标范围
        const loader = new THREE.TextureLoader().setPath('static/model/img/')
        loader.load('destinationBorder.png', texture => {
            destination.material = new THREE.MeshBasicMaterial({
                map: texture,
                transparent: true,
                opacity: 0.9,
                side: THREE.DoubleSide,
                depthTest: false
            })
            const destinationBorder = new THREE.Mesh(new THREE.CircleGeometry(2.5, 50), destination.material)
            destination.add(destinationBorder)
        })
        this.destination = destination

        this.addOn(obj, 'mousemove',
            (objList) => {
                that.destination.position.copy(objList[0].point)
                that.destination.visible = true
            }, 'mousemove_destination',
            null,
            () => {
                that.destination.visible = false
            })

        that.addOn(destination, 'click',
            (objList) => {
                const x = that.destination.position.x
                const z = that.destination.position.z
                const y = that.camera.position.y
                const v = that.camera.position.clone().sub(that.orbitControls.target.clone()) // 差
                that.flyTo({
                    position: [x, y, z],
                    controls: [x - v.x, y - v.y, z - v.z],
                    duration: 500,
                    done: function () {
                    }
                })
            }, 'click_destination')
    }

    /**
     * 判断是否为移动（手机平板）设备
     * @returns {boolean}
     */
    isMobile() {
        if (navigator.userAgent.toLowerCase().match(/(ipod|ipad|iphone|android|coolpad|mmp|smartphone|midp|wap|xoom|symbian|j2me|blackberry|wince)/i) != null) return true
        return false
    }

    /**
     * 判断是否连接网络
     * @return {boolean}
     */
    isNetwork() {
        if (navigator.onLine) return true
        return false
    }

    /**
     * 获取网络连接类型
     * @return {null|*}
     */
    getNetworkType() {
        if (this.isNetwork()) return navigator.connection.type
        return null
    }

    /**
     * 寻找切换断点
     * @protected
     * @param text String 文本内容
     * @param width 每行宽度
     * @param context object
     * @return {number}
     */
    findBreakPoint(text, width, context) {
        var min = 0
        var max = text.length - 1

        while (min <= max) {
            var middle = Math.floor((min + max) / 2)
            var middleWidth = context.measureText(text.substr(0, middle)).width
            var oneCharWiderThanMiddleWidth = context.measureText(text.substr(0, middle + 1)).width
            if (middleWidth <= width && oneCharWiderThanMiddleWidth > width) {
                return middle
            }
            if (middleWidth < width) {
                min = middle + 1
            } else {
                max = middle - 1
            }
        }
        return -1
    }

    /**
     * 字符串按宽度拆分数组
     * @protected
     * @param text  String 文本内容
     * @param context object
     * @param width 每行宽度
     * @return {[]}
     */
    breakLinesForCanvas(text, context, width) {
        var result = []
        var breakPoint = 0

        while ((breakPoint = this.findBreakPoint(text, width, context)) !== -1) {
            result.push(text.substr(0, breakPoint))
            text = text.substr(breakPoint)
        }

        if (text) {
            result.push(text)
        }

        return result
    }

    /**
     * 声明
     */
    statement() {
        console.log(['%cThreeUtilApp', '%c更新时间: 2021-04-11', '%c作者: 梦辰雪', '%c版本号: v1.0', '%cgitee: https://gitee.com/mcxgitee'].join('\n').toString(),
            `
            background:url() no-repeat left center;
            background-size:30px 40px;
            padding-left:40px;
            line-height:50px;
            font-size: 18px;
            font-weight:bold;
            color:#00D4FF
            `,
            `
            background:none;
            line-height:30px;
            font-size: 18px;
            font-weight:bold;
            color:#00D4FF
            `,
            `
             padding-left:40px;
            background:none;
            line-height:30px;
            font-size: 18px;
            font-weight:bold;
            color:#00D4FF
            `,
            `
            background:none;
            line-height:30px;
            font-size: 18px;
            font-weight:bold;
            color:#00D4FF
            `,
            `
            padding-left:40px;
            background:none;
            line-height:30px;
            font-size: 18px;
            font-weight:bold;
            color:#00D4FF
            `
        )
    }
}
